package org.stdoutremover;

import java.util.ArrayList;
import java.util.Iterator;

import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldInsnNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;

public final class StdoutRemovalTransformer 
{
	// TODO use primitive stack instead
	private final ArrayList<int[]> deletionBoundaries;
	
	public StdoutRemovalTransformer() 
	{
		this.deletionBoundaries = new ArrayList<int[]>();
	}


	/**
	 * Removes all System.out.println() statements from the given class
	 * 
	 * @param cn
	 */
	public void transform(final ClassNode cn) 
	{
		AbstractInsnNode insnNode;
		FieldInsnNode    fieldInsnNode;
		MethodInsnNode 	 methodInsnNode;
		
		final Iterator<MethodNode> methodIter = cn.methods.iterator();
		while(methodIter.hasNext())
		{
			final MethodNode methodNode = methodIter.next();
			
			final InsnList insnList = methodNode.instructions;
			final int      numInsn  = insnList.size();
			
			int begin = -1;
			
			
			//--- look for deletion boundaries -> instructions in this range will be deleted because they belong to the corresponding System.out.println() statement
			for(int i = 0; i < numInsn; i++)
			{
				insnNode = insnList.get(i);
				
				if(insnNode instanceof FieldInsnNode) // typical beginning of a System.out.println() statement
				{
					if(insnNode.getOpcode() == Opcodes.GETSTATIC)
					{
						fieldInsnNode = (FieldInsnNode) insnNode;
					
						if("java/lang/System".equals(fieldInsnNode.owner)						  && 
						   ("out".equals(fieldInsnNode.name) || "err".equals(fieldInsnNode.name)) && 
						   "Ljava/io/PrintStream;".equals(fieldInsnNode.desc))
						{
							begin = i;
						}
						else
						{
							begin = -1;
						}
					}
				}
				else if(insnNode instanceof MethodInsnNode) // typical ending of a System.out.println() statement
				{
					if(begin != -1)
					{
						if(insnNode.getOpcode() == Opcodes.INVOKEVIRTUAL)
						{
							methodInsnNode = (MethodInsnNode) insnNode;
							
							if("java/io/PrintStream".equals(methodInsnNode.owner) &&
								("println".equals(methodInsnNode.name) || "print".equals(methodInsnNode.name)))
							{
								deletionBoundaries.add(new int[]{begin, i});
								begin = -1;
							}							
						}
					}
				}
			}
			
			//--- perform deletion of all involved statements
			
			if(! this.deletionBoundaries.isEmpty())
			{
				// NOTE: we can assume that the boundaries are given in sorted order
				final Iterator<MethodInsnNode> insIter = methodNode.instructions.iterator();
				
				int   	   delRangePtr       = 0;
				final int  delBoundariesSize = deletionBoundaries.size();
				int[]      range;
				
				begin   = -1;
				int end = -1;
				
				int i = 0;
				while(insIter.hasNext())
				{
					insIter.next();
					
					if(i > end)
					{
						if(delRangePtr == delBoundariesSize)
						{
							// all System.out.println() related instructions are deleted from this method
							break;
						}
						
						// handle next System.out.println() related instructions
						range = this.deletionBoundaries.get(delRangePtr++);
						begin = range[0];
						end   = range[1];
					}		
					
					if(i >= begin && i <= end)
					{
						// remove instruction
						insIter.remove();
					}
						
					i++;
				}
				
				// clear data structure for re-usage
				this.deletionBoundaries.clear();
			}
		}
	}
}